fix(plugin-list): ObjectGallery waits for the object definition instead of querying twice (objectui#7903) - #7919
Merged
Conversation
…ad of querying twice (objectui#7903) `ObjectGallery` sat outside the set objectui#6482 converged on the shared settled-schema gate, and nothing marked it a deliberate exclusion. It held the object definition in a local `useState` fed by its own metadata effect and listed that definition in the record-fetch effect's dependency array, so every object-bound load issued two `find` calls: the first before the definition landed, with `buildExpandFields` seeing no fields and therefore carrying no `$expand` at all, and a second one after. The first paint was a grid of cards rendered from raw foreign-key ids. Measured on this component rather than inherited from the matching shape (objectui#6482's own per-component standard): instrumented renderer, one mount per hold, `getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with `ObjectCalendar` as a positive control in the same run. Before: 2 `find` calls with expand sets [null, ['owner']] at every hold, order always schema:issued / find(unexpanded) / schema:settled / find(expanded), two painted states, 3 late writes, first paint flat at 3-7 ms across the whole sweep. After: 1 `find` carrying ['owner'], one painted state, 0 late writes, first paint tracking the hold (9 ms at +3, 35 ms at +25, 106 ms at +100). The cost here is a two-step paint, not the three-step one `ObjectCalendar` and `ObjectTimeline` measured: their `loading` is an unconditional early return, so the re-run drops them back to a placeholder; this component's is `loading && !items.length`, so the raw ids were replaced in place. The resolution half is now `useSettledSchema` from `@object-ui/react`, which settles on every exit -- no source, no `getObjectSchema`, no object name, and a read that threw alike. The replaced effect returned without settling on all four (objectui#7232), which cost nothing while nothing waited on it and would hold a gated query open forever. Two departures, judged for this component rather than copied from a sibling: the metadata read is NOT disabled on the authored-items path, because this component reads the definition on every path for cell semantics and ADR-0079 card titles; and the gate branch holds the loading placeholder, which the two siblings get from their initial `loading` state and this one does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7903
ObjectGallerysat outside the set objectui#6482 converged on the sharedsettled-schema gate —
ObjectKanban,ObjectView,ObjectCalendarandObjectTreewere named there,ObjectGanttwas ask 2 of objectui#7225, andObjectTimelinewas objectui#7895 — and nothing marked it a deliberateexclusion. It held the object definition in a local
useStatefed by its ownmetadata effect and listed that definition in the record-fetch effect's
dependency array.
The card said "shape reading, NOT measured" — so it was measured first
The filing seat spotted the shape while working objectui#7895 and did not
measure it. Instrumented renderer (a
React.Profilercommit counter reading therendered card grid), writes counted per single mount,
getObjectSchemaheldat a fixed delay, grid
0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, withObjectCalendaras a positive control in the same vitest run.findcallsObjectGallery[null, ['owner']]ObjectGallery[['owner']]ObjectCalendar(control)[['owner']]ObjectCalendar(control)[['owner']]Every reading held at every one of the fifteen holds, not one band of them.
reproduces exactly:
[null, ['owner']]. The first call goes out with noexpansion, so the first paint is raw foreign-key ids. The issue order was
schema:issued, find(unexpanded), schema:settled, find(expanded)at everyhold including
+0 ms; after the gate it isschema:issued, schema:settled, find(expanded).writes into the card grid after the first paint, at every hold. After: one
state, 0 late writes.
across a 0 to 100 ms sweep — it painted before the metadata, which is the
defect. After, it tracks: 9 ms at
+3, 15 ms at+10, 35 ms at+25, 106 msat
+100. The control tracked the hold in both directions (33-69 ms at lowholds, 77 ms at
+50, 161 ms at+100), so the instrument reads a live gaterather than a flat machine.
The late-write lower edge sat where objectui#7895 measured it (nothing at
+0to
+2, stable from+3up) once the readiness predicate is what counts them;counted from the first paint instead, the 3 late writes are present at every
hold. The grid was refined rather than repeated: no hold measured nothing.
Measured here rather than inherited from the matching shape — objectui#6482's own
per-component standard.
ObjectCalendar(objectui#6453) andObjectTimeline(objectui#7895) both make
loadingan unconditional early return, so there-run's
setLoading(true)drops them back to their placeholder between the twopaints. Here the early return is
loading && !items.length: once the first (raw)rows are in state the skeleton cannot come back. Measured
skelAfter=falseatevery hold — the user saw raw foreign-key ids replaced in place by the
expanded rows.
The two deliberate departures, each judged for this component
objectui#7895 made two departures from
ObjectCalendar/ObjectGantt. Bothapply here, and each for a stronger reason than on the timeline:
dataSourceis passed unconditionally, not
hasInlineData ? undefined : dataSource. Yes,ObjectGalleryhas an authored path — three of them (props.data,schema.data,bind) — and it reads the definition on every one:buildEnrichedFieldreadsobjectDef.fieldsfor each visible field's type,options, currency, precision and reference target, and
getRecordDisplayName(objectDef, item)resolves each card's title underADR-0079. Disabling the read would strip cell semantics and card titles off
exactly the paths that issue no query.
schema.objectName, notresolveRecordSourceObjectName.No,
ObjectGallerydoes not resolve adatablock. That reader's second rungis a resolved provider config (
dataConfig.provider === 'object'), andObjectGallerySchema['data']is typedRecord<string, unknown>[]— a bareinline record array, not a provider config. The component calls
getDataConfignowhere, so the ladder would degenerate toschema.objectNamewith extra spelling, while
dataSource.find(schema.objectName, …)is the namethe query itself uses.
A third decision, this component's own, named rather than smuggled.
loadingstarts
falsehere and was only ever flipped inside the fetch, so a barereturnat the gate would show "No items to display" for the whole metadata read— a false empty state where the pre-gate component showed the loading
placeholder. The two siblings get this from their initial state
(
ObjectCalendarstartsloadingattrue;ObjectTimelinecomputes it in alazy initializer). Rather than restructure this component's initial state — which
would change what
schema.data: []andprops.data: []render — the guaranteeis stated in the one branch that knows a query is coming, and pinned by its own
case. This is not a fourth gate shape: the gate is the converged
useSettledSchema+if (!ready) returnin the querying branch.Ablation
The fix was committed first, then
packages/plugin-list/src/ObjectGallery.tsxwas reverted with a pinned commit sha (
6a9ee323e, the merge-base — never amoving remote-tracking name).
git hash-objectread49ee0b6f821ed6d51122b40c850d43661f33bf5e, equal to the pinned commit's bloband different from HEAD's
9a0ddc5419733eb3de938ef20645d217c46e04bc. Anchorcounts under mutation:
useSettledSchema0,objectDefReady0,setObjectDef2.
git diff HEADplus a disk blob equal toHEAD:packages/plugin-list/src/ObjectGallery.tsx(
9a0ddc5419733eb3de938ef20645d217c46e04bc), withgit status --porcelainempty and the anchors back at 3/3/0. The restore ran from a
trap … EXIT INT TERMusing an absolute path resolved fromgit rev-parse --show-toplevel.instrument imports
../ObjectGalleryby relative path, and the rootvitest.config.mtsalias table points every@object-ui/*specifier at thatpackage's
src/(@object-ui/react→packages/react/src), so nodistison the resolution path. The ablation is itself the proof: the source mutation
turned the pins red with no build step anywhere, which a dist-resolved
suite could not have done.
Prediction was recorded before the run: 5 red, 2 green. Observed: 5 red, 2
green — the same five.
expected "vi.fn()" to be called 1 times, but got 2 timesexpected undefined to deeply equal [ 'owner' ]getObjectSchemaorigin/mainhas no gate, so it queries here anyway — it guards a FUTURE wrong shape, going red the moment anyone "simplifies" the gate toif (!objectDef) return;expected '' to contain '[useSettledSchema]'— the replaced effect loggedconsole.warn('Failed to fetch object def for ObjectGallery'), the hook logsconsole.error('[useSettledSchema] …'). Predicted, and the same log-wording asymmetry objectui#7895 reported rather than tuned awaydatapathexpected [ 'find', 'paint', 'definition', …(3) ] to deeply equal [ 'definition', 'find', 'paint' ]expected "vi.fn()" to not be called at all, but actually been called 1 timesSecond-order payoff: there is no sibling ladder fixture to re-measure
Reported whichever way it came out. The
colorFieldLadder-7243fixture set hasexactly three members —
ObjectCalendar,ObjectGantt,ObjectTimeline— thethree renderers that consume
createFieldColorResolverfrom core.packages/plugin-list/contains no member of it (grepforcolorFieldLadder/createFieldColorResolver/7243across the packagereturns nothing), because
ObjectGalleryrenders option colours throughgetCellRendererinstead. Nor does any of the three existing gallery tests carrythe pre-#7521 harness shape: the module-level shared-capture harness exists in
this package only in
ListView.*files, none of which mountObjectGallery. Sothere is nothing here that would still redden under the pre-#7521 harness shape,
and no guard was deleted or relaxed — a guard's value does not depend on
there being a live trigger today.
Scope
Fetch sequencing only, as ruled. Untouched: objectui#7390's unbounded
fetch /
ObjectGallerySchemapaging surface on this same file (ruled notdispatchable), objectui#7429's
$expand/ FLS question, andObjectTimeline/ObjectGantt/ObjectCalendar.Verification
All on the pushed commit
b1b3c4b20, withgit diff HEADempty.vitest run packages/plugin-list/— 68 files, 860 tests passed.vitest run …/ObjectGallery.fetchGate-7903.test.tsx— 7 passed.pnpm --filter @object-ui/plugin-list type-check— exit 0, runningtsc --noEmit && tsc -p tsconfig.test.json. Proven non-vacuous:tsc -p tsconfig.test.json --listFilesnames the new pin file, the component,and the built
useSettledSchemadeclaration.(
pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build).eslint --no-inline-configon both changed files: 0 errors. The singlereact-hooks/set-state-in-effectwarning is pre-existing, at the untouchedsetFetchedData(props.data)line.node scripts/check-changeset-presence.mjs— verdict quoted verbatim:✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7903-gallery-settled-schema-gate.md.The
patchbump follows the checker's own ceiling(
Scored minor at most, never major).node scripts/check-changeset-no-major.mjs—✅ No changeset declares a major bump.node scripts/check-governed-queue-guard.mjs --test …— verdict verbatim:✅ NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.An ordinary pull request: the normal review and merge-queue route applies.commit, all exit 0:
check:control-bytes,check:self-import,check:phantom-deps,check:esm-specifiers,check:vi-mock-specifiers,check:vi-mock-inherit,check:unreferenced-sources,check:element-data-source-declaration. Their scanned populations moved withthe commit (control-bytes 6396 → 6398 tracked text files; the vi-mock gates
4331 → 4332 source files), so the new files are inside what they graded.
Declared narrowing: the repo-wide
pnpm lintscan is left to CI, which runsthe farm exactly once regardless. Local linting was scoped to the two changed
files.
🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code